对象包装

我为你在课程中取得这么大的进步感到非常自豪。到目前为止,您已经了解了所有基础知识。不仅如此,您还了解了“Hello World”程序的工作原理。现在是时候进一步讨论一些高级主题了。在本课程中,我们将介绍对象包装,当您深入在 Sui 上构建 dApp 时,它将为您提供极大帮助。

对象包装

在 move 编程语言中,您可以通过一种称为包装的方法将一个数据结构放入另一个数据结构中来组织数据结构。想象一下,这就像一个工具箱,里面装有不同的物品。在 move 中,您可以通过将 struct 类型的字段放在另一个字段中来组织数据结构,如下所示:

#![allow(unused)]
fn main() {
struct Foo has key {
    id: UID,
    bar: Bar,
}

struct Bar has key, store {
    id: UID,
    value: u64,
}
}

对于要成为 Sui 对象的结构,它应该具有 key 能力。现在,如果将 Bar 类型的 Sui 对象放入 Foo 类型的 Sui 对象中,则对象类型 Foo 会包装对象类型 Bar .对象类型 Foo 是包装器或包装对象。

在 move 代码中,您可以将 Sui 对象作为非 Sui 对象结构类型的字段。

  • 想象一下,您有一个名为“Foo”的结构,它不是 Sui 对象,还有另一个名为“Bar”的结构,它是具有“key”和“store”功能的 Sui 对象。您可以将“Bar”放入“Foo”中,但这只是暂时的,不会持久保留在区块链上。
  • 当你包装一个对象时,它就成为包装对象数据的一部分。这意味着您无法通过其唯一 ID 查找它,也无法将包装对象作为区块链交易中的参数传递。访问它的唯一方法是通过包装它的对象。
  • 例如,您不能创建这样的情况:对象 A 包装对象 B、B 包装 C、C 也包装 A。这会导致代码复杂性和问题。
  • 您可以“解开”包裹的对象。当你解开它时,它又变成了一个独立的对象,可以直接在区块链上访问。该对象保留与包装之前相同的唯一 ID。

有几种常见方法可以将 Sui 对象包装到另一个 Sui 对象中。让我们看看下面的内容:

直接包装

如果将一个 Sui 对象类型直接作为另一个 Sui 对象类型中的字段,则称为直接包装。在这种类型的包装中,除非包装对象被销毁,否则无法解开包装对象。它可用于锁定访问权限有限的对象。

让我们看一个例子。假设您在 Sui 中有一种名为 Object 的 NFT 样式对象,它具有 scarcitystyle 等属性。为了确保公平交易,你需要确保稀缺性相同,只有款式不同。这是因为 NFT 的稀缺性越高,其价值就越高。以下是实现它的方法:

#![allow(unused)]
fn main() {
public struct Object has key, store {
    id: UID,
    scarcity: u8,
    style: u8,
}

public entry fun create_object(scarcity: u8, style: u8, ctx: &mut TxContext) {
    let object = Object {
        id: object::new(ctx),
        scarcity,
        style,
    };
    transfer::transfer(object, tx_context::sender(ctx));
}
}

在上面的代码中,您可以使用 create_object 函数创建具有指定稀缺性和样式的对象。任何人都可以调用它,创建的对象将发送给交易发送者。

现在,您可能还记得前面的部分,只有对象所有者才能发送交易来改变对象。因此,一个人无法发送一笔将自己的对象与他人的对象交换的交易。因此,任何想要交换其对象的人都需要将其对象发送到第三方(例如交换站点)。为了确保您保留对您的对象(例如硬币和 NFT)的保管权并且不将全部保管权交给第三方,您需要使用直接包装,如下所示:

#![allow(unused)]
fn main() {
public struct ObjectWrapper has key {
    id: UID,
    original_owner: address,
    to_swap: Object,
    fee: Balance<Sui>,
}
}

ObjectWrapper 包装要交换 to_swap 的对象并跟踪其原始所有者。您可能还需要支付交换费用。

要请求交换,您需要定义一个入口函数:

#![allow(unused)]
fn main() {
public entry fun request_swap(object: Object, fee: Coin<Sui>, service_address: address, ctx: &mut TxContext) {
    assert!(coin::value(&fee) >= MIN_FEE, 0);
    let wrapper = ObjectWrapper {
        id: object::new(ctx),
        original_owner: tx_context::sender(ctx),
        to_swap: object,
        fee: coin::into_balance(fee),
    };
    transfer::transfer(wrapper, service_address);
}
}

在此函数中,要交换的对象使用 ObjectWrapper 包装,并指定费用。然后将包装器发送到服务地址。

这里的关键是,即使服务运营商拥有 ObjectWrapper,他们也无法访问或窃取其中包装的“对象”。这是因为 Sui 不允许在没有特定的解包函数的情况下访问包装对象,而该函数未在此模块中定义。

通过 Option包装

在上面的直接包装示例中,您必须销毁一个对象才能将其内部的对象取出。但有时,您可能需要更大的灵活性,其中包装类型并不总是在其中包含包装对象,并且您可以在需要时将包装对象替换为其他对象。

让我们举一个拥有剑和盾牌的战士的例子。他应该可以选择拥有或不拥有它们。他应该能够添加新的剑和盾牌或替换当前的剑和盾牌。

在下面的代码中,我们定义了一个带有可选 swordshield 字段的 SimpleWarrior 类型。 swordshield 是其他 Sui 对象类型。

#![allow(unused)]
fn main() {
public struct SimpleWarrior has key {
    id: UID,
    sword: Option<Sword>,
    shield: Option<Shield>,
}

public struct Sword has key, store {
    id: UID,
    strength: u8,
}

public struct Shield has key, store {
    id: UID,
    armor: u8,
}
}

当你创建一个新的战士时,你一开始没有任何装备:

#![allow(unused)]
fn main() {
public entry fun create_warrior(ctx: &mut TxContext) {
    let warrior = SimpleWarrior {
        id: object::new(ctx),
        sword: option::none(),
        shield: option::none(),
    };
    transfer::transfer(warrior, tx_context::sender(ctx))
}
}

此函数 create_warrior 创建一个没有剑或盾的新 SimpleWarrior

要装备新的剑或盾,您可以定义如下函数:

#![allow(unused)]
fn main() {
public entry fun equip_sword(warrior: &mut SimpleWarrior, sword: Sword, ctx: &mut TxContext) {
    if (option::is_some(&warrior.sword)) {
        let old_sword = option::extract(&mut warrior.sword);
        transfer::transfer(old_sword, tx_context::sender(ctx));
    };
    option::fill(&mut warrior.sword, sword);
}
}

在上面的函数 equip_sword 中,您传递了战士作为可变引用和一把包裹到战士中的剑。如果战士已经拥有一把剑,它会首先移除旧剑并将其发送回发送者。然后,它将新剑包裹在战士体内。这样,战士在装备新东西时就可以更换他们的装备。

通过vector包装

在 Sui 中,您可以将对象包装在另一个 Sui 对象的向量场中,这类似于使用 Option 类型包装对象。这种方法允许一个对象包含零个、一个或多个相同类型的包装对象。

让我们考虑一个涉及宠物和农场的例子:

#![allow(unused)]
fn main() {
public struct Pet has key, store {
    id: UID,
    cuteness: u64,
}

public struct Farm has key {
    id: UID,
    pets: vector<Pet>,
}
}

在此代码中,我们有两种对象类型:

  1. Pet :代表个体宠物,具有唯一的ID和“可爱度”值,这只是宠物特征的占位符。
  2. Farm :代表一个农场,也有一个唯一的ID。这里的关键特征是 pets 字段,它是 Pet 对象的向量。这意味着一个农场可以饲养多只宠物。

通过此设置,您可以将多个宠物添加到农场。每个宠物都被包裹在一个 Pet 对象中,这些对象被收集并存储在farm对象的 pets 字段中。这允许您通过农场对象管理和访问与特定农场关联的所有宠物。

小结

在本课程中,您已经完全掌握了对象包装。您非常清楚它是什么以及如何在 Move on Sui 中使用它。在下一课中,我们将继续学习 Move on Sui 中的更多高级主题和学习活动。

quiz

“直接包装”和“通过选项包装”之间的主要区别是什么?